4.1 从代码到二进制:Go程序的编译过程

本节我们将一起探索,Go程序在编译的时候都发生了什么、都做了哪些工作?通过本节的学习将对我们的日常编程规范、习惯起到一个正向的作用。

关于编译过程,我们将会从词法分析、语法分析、语义分析以及中间代码生成等多个方面进行讲解。

词法分析

词法分析是编译的第一阶段。在这一阶段中,编译器会将代码转换为一系列标记,在Go语言中叫tokens

在这个阶段,编译器会去除空格、注释,并标记出各种代码元素,如关键字(funcvar 等)、标识符、字面量(数字、字符串等)。

我们可以通过源码cmd/compile/internal/syntax查看到相应的实现。简单来说,就是将我们的代码进行一个整理,整理为Go语言后续编译可识别的代码。


我们以一个例子来说明,看一下编译var a int = 10时会发生什么。

  • 首先分析器会逐字符的读取到我们的源码var a int =10,这时候读取到的是varint=10

  • 下一步根据关键字进行匹配,将会得到结果:varaint10

  • 在上一步中已经将这一句代码整理得到了几个关键的元素,那么下一步就是针对这几个元素作标记。如下所示:

      var:
      标记类型:keyword(关键字)
      代表 Go 语言中的变量声明关键字 var。
    
      a:
      标记类型:identifier(标识符)
      代表变量名 a。
    
      int:
      标记类型:type(类型)
      代表变量的类型 int。
    
      =:
      标记类型:operator(操作符)
      代表赋值操作符 =。
    
      10:
      标记类型:integer(整数常量)
      代表整数常量 10
  • 标记完成以后,将会进一步整合形成token格式,如下所示:

      [
          {Token: "var", Type: "keyword"},
          {Token: "a", Type: "identifier"},
          {Token: "int", Type: "type"},
          {Token: "=", Type: "operator"},
          {Token: "10", Type: "integer"}
      ]
    

通过上面的例子,我们大概可以知道这一步的真正作用。简单来说就是将源代码转换为编译器需要的类型。

语法、语义分析

在词法分析后,下一步就会进入到语法分析中。语法分析的主要作用是验证语法的正确性、生成语法树、报告语法错误。

我们继续上文的案例,如下所示:

  • 语法分析接收到tokens结果后,根据规则检查每一个类型定义是否正确、符号是否正确

  • 检查通过后生成语法树,如下所示:

      VarDecl
      ├── Identifier: "a"
      ├── Type: "int"
      └── Value: 10
    

从上面的示例我们大概可以看出,语法分析会将之前的代码进一步转换,转换为了语法数的结构,这也就是为下一步的工作继续做准备。

假如我们代码是这样的:a = b + c * d,经过词法分析后输出如下:

[
    {Token: "a", Type: "identifier"},
    {Token: "=", Type: "operator"},
    {Token: "b", Type: "identifier"},
    {Token: "+", Type: "operator"},
    {Token: "c", Type: "identifier"},
    {Token: "*", Type: "operator"},
    {Token: "d", Type: "identifier"}
]

那么经过语法分析后生成的语法树如下所示:

Assignment
├── Identifier: "a"
└── Expression: "+"
    ├── Left: "b"
    └── Expression: "*"
        ├── Left: "c"
        └── Right: "d"

这个语法树表示了表达式a = b + (c * d)的结构,显示了乘法操作比加法操作优先级更高,并明确了赋值操作的目标是a

总的来说,就是语法分析器会将代码进一步转换为一个树结构,方便后续的进一步编译操作。

中间代码生成

我们知道,最终编译形成的程序都是机器码。那么中间代码其实就是指源代码与机器码之间的一个节点。

这个节点的代码不属于源代码,也不是机器码,而是介于两者之间的,独立于机器架构的,仅仅只是单纯的代码表示。

该阶段可以从源代码包:cmd/compile/internal/ir去查看学习。

比如我们的代码如下:

func main() {
    a := 10
    b := 20
    c := a + b
}

那么中间代码可能变成这样:

t1 = 10
t2 = 20
t3 = t1 + t2

t1对应变量a的赋值,t2对应变量b的赋值,t3对应变量c的赋值和a + b的计算结果。

在比如我们上文提到的:a = b + (c * d),可能中间代码会变成这样:

t1 = c * d
t2 = b + t1
a = t2

优化

优化阶段,指的是编译器对之前生成的中间代码进行优化,以提高程序的执行效率。

优化的目标包括消除冗余代码、简化表达式、减少内存访问等。

优化可以分为局部优化和全局优化,分别针对局部代码块和整个程序进行优化。

这一部分的实现代码可以在源码包:cmd/compile/internal/ssa中查看学习。

Go语言中优化主要包括局部优化、全局优化、循环优化以及内存优化等方面。在这里我们主要说一下局部优化,方便我们理解这一阶段的工作。

  • 常量折叠:在编译时计算常量表达式的结果,以减少运行时的计算。例如,将3 + 5直接替换为8

      Original: a = 3 + 5
      Optimized: a = 8
    
  • 常量传播:将已知的常量值传播到它们使用的地方,减少冗余计算。

      Original: a = 8
             b = a + 4
      Optimized: b = 12
    
  • 死代码消除:删除永远不会被执行的代码或计算结果不会被使用的代码。

      Original: a = 3
             b = a * 2
             c = 4
             b = a + c
      Optimized: a = 3
              b = 7
    

通过上面的例子我们可以看出,这一阶段其实就是简化、优化代码。

比如说一些在代码中的计算,如果结果与运行时无关联,是固定的,那么在编译阶段就会计算完成,减少运行时的计算消耗。

代码生成、汇编与链接

这一部分也是比较重要的一个阶段,在这个阶段就会将之前的优化代码转换,形成最终的机器码。

在这一部分包含汇编与链接两个部分,可以在源码包:cmd/asmcmd/link查看学习。

在这里我们简单的说一下执行步骤:

  • 编译器将源代码编译生成中间代码,并进一步生成汇编代码。

  • 汇编器将汇编代码转化为目标文件,包含机器指令和符号表。

  • 链接器将多个目标文件和库文件链接在一起,解决符号引用,形成最终的可执行文件。

小结

Go程序的编译过程包括多个阶段,从源代码的词法分析、语法分析到最终的代码生成和可执行文件的生成。每个阶段都负责不同的任务,确保源代码被正确编译为高效的机器码。

流程为:

词法分析 -> 语法分析 -> 语义分析 -> 中间代码生成 -> 优化 -> 代码生成 -> 汇编与链接

关于本节总结如下:

  • 词法分析将源代码整合为标准代码流

  • 语法分析检查代码流语法,形成代码树

  • 语义分析检查代码语义是否正确、逻辑是否合理

  • 中间代码生成对代码进行了进一步的转换

  • 优化模块对代码检查优化,提升代码性能

  • 优化后将中间代码转换为汇编代码或机器码

  • 最终通过链接形成最终可执行文件

results matching ""

    No results matching ""